/*
 * JFugue - API for Music Programming
 * Copyright (C) 2003-2008  David Koelle
 *
 * http://www.jfugue.org 
 * 
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or any later version.
 * 
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 * 
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
 *  
 */

package org.jfugue;

import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.sound.midi.MetaMessage;
import javax.sound.midi.MidiEvent;
import javax.sound.midi.MidiMessage;
import javax.sound.midi.Sequence;

public final class TimeFactor {
	final public static double DEFAULT_BPM = 120.0d;
	final public static int QUARTER_DURATIONS_IN_WHOLE = 4;

	public static final double getTimeFactor(Sequence sequence, double bpm) {
		double divisionType = sequence.getDivisionType();
		double resolution = sequence.getResolution();

		// If division type is PPQ, resolution is ticks per beat.
		// Since a beat is the length of time given to a one quarter note, this
		// essentially
		// means that ticks per beat == pulses per quarter note (PPQ or PPQN)
		if (divisionType == Sequence.PPQ) {
			// System.out.println("DivisionType is PPQ");
			// System.out.println("Resolution is "+resolution);
		} else {
			// System.out.println("DivisionType is SMPTE");
		}
		// Useful resources: http://www.borg.com/~jglatt/tech/midifile/tempo.htm
		// and http://www.borg.com/~jglatt/tech/midifile/ppqn.htm

		// If bit 15 of division is a zero, the bits 14 thru 0 represent the
		// number of delta-time ticks which make up a
		// quarter-note. For instance, if division is 96, then a time interval
		// of an eighth-note between two events
		// in the file would be 48.
		//
		// If bit 15 of division is a one, delta-times in a file correspond to
		// subdivisions of a second, in a way consistent
		// with SMPTE and MIDI time code. Bits 14 thru 8 contain one of the four
		// values -24, -25, -29, or -30, corresponding
		// to the four standard SMPTE and MIDI time code formats (-29
		// corresponds to 30 drop frame), and represents the number
		// of frames per second. These negative numbers are stored in two's
		// complement form. The second byte (stored positive)
		// is the resolution within a frame: typical values may be 4 (MIDI time
		// code resolution), 8, 10, 80 (bit resolution),
		// or 100. This system allows exact specification of time- code-based
		// tracks, but also allows millisecond-based tracks
		// by specifying 25 frames/sec and a resolution of 40 units per frame.
		// If the events in a file are stored with bit
		// resolution of thirty-frame time code, the division word would be E250
		// hex.

		if (bpm == 0.0) {
			bpm = DEFAULT_BPM;
		}

		return 60000.0 / (resolution * bpm);
	}

	public static final byte[] convertToThreeTempoBytes(int tempo) {
		double tempoInMsPerBeat = TimeFactor
				.convertBPMToMicrosecondsPerBeat(tempo);
		double d1 = Math.floor(tempoInMsPerBeat / 16384.0);
		double d2 = Math.floor((tempoInMsPerBeat % 16384.0) / 128.0);
		double d3 = Math.floor((tempoInMsPerBeat % 16384.0) % 128.0);
		return new byte[] { (byte) d1, (byte) d2, (byte) d3 };
	}

	public static final int parseMicrosecondsPerBeat(MetaMessage message,
			long timestamp) {
		int tempo = message.getData()[0] * 16384 + message.getData()[1] * 128
				+ message.getData()[2];
		int beatsPerMinute = (int) convertMicrosecondsPerBeatToBPM(tempo);
		return beatsPerMinute;
	}

	/** Converts microseconds per beat to BPM -- and vice versa */
	public static final double convertMicrosecondsPerBeatToBPM(double value) {
		double microsecondsPerMinute = 60000000.0D;
		if (value == 0.0d) {
			return 0.0d;
		}
		return microsecondsPerMinute / value;
	}

	/** Converts microseconds per beat to BPM -- and vice versa */
	public static final double convertBPMToMicrosecondsPerBeat(int bpm) {
		double microsecondsPerMinute = 60000000.0D;
		if (bpm == 0) {
			return 0;
		}
		return microsecondsPerMinute / bpm;
	}

	/**
	 * Takes all of the MIDI events in the given Sequence, sorts them according
	 * to when they are to be played, and sends the events to the
	 * MidiMessageRecipient when the each event is ready to be played.
	 * 
	 * @param sequence
	 *            The Sequence with messages to sort and deliver
	 * @param recipient
	 *            the handler of the delivered message
	 */
	public static final void sortAndDeliverMidiMessages(Sequence sequence,
			MidiMessageRecipient recipient) {
		double timeFactor = 1.0;

		Map<Long, List<MidiEvent>> timeMap = new HashMap<Long, List<MidiEvent>>();
		long longestTime = TimeEventManager.sortSequenceByTimestamp(sequence,
				timeMap);

		long lastTime = 0;
		for (long time = 0; time < longestTime; time++) {
			List<MidiEvent> midiEventList = timeMap.get(time);
			if (midiEventList != null) {
				for (MidiEvent event : midiEventList) {
					MidiMessage message = event.getMessage();
					if ((message.getMessage().length >= 2)
							&& (message.getMessage()[1] == 0x51)
							&& (message instanceof MetaMessage)) {
						int bpm = parseMicrosecondsPerBeat(
								(MetaMessage) message, time);
						timeFactor = TimeFactor.getTimeFactor(sequence, bpm);
						System.out.println("TimeFactor is " + timeFactor);
					}
					recipient.messageReady(message, time);
				}

				try {
					long sleepTime = (int) (((time - lastTime)
							* (TimeFactor.QUARTER_DURATIONS_IN_WHOLE + 0.20)));
					Thread.sleep(sleepTime); // (int) (1 * timeFactor));
					lastTime = time;
				} catch (Exception ex) {
					throw new JFugueException(JFugueException.ERROR_SLEEP);
				}
			}
		}
	}
}
